#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
作者: jena
创建时间: 2025.10
数据处理API服务器 - FastAPI(ASGI) 离线优化版本
提供纯 FastAPI 的 RESTful API 接口
"""

import os
import json
import uuid
import traceback
import logging
import re
from typing import Dict, List, Any, Optional
from pathlib import Path
from datetime import datetime

# 依赖管理
try:
    from src.common_utils import FastAPIDependencyManager
except ImportError:
    from .common_utils import FastAPIDependencyManager

deps = FastAPIDependencyManager()
if not deps.has_fastapi():
    raise ImportError("FastAPI不可用，ASGI服务器无法启动。请检查离线依赖包安装。")

FastAPI = deps.get_fastapi()
Request = deps.get_request_cls()
responses = deps.get_responses()
JSONResponse = responses.get('JSONResponse')
FileResponse = responses.get('FileResponse')
PlainTextResponse = responses.get('PlainTextResponse')
CORSMiddleware = deps.get_cors_middleware()
StaticFiles = deps.get_static_files()

# 导入核心模块
try:
    from src.character_cleaner import CharacterCleaner
    from src.sentence_cleaner import SentenceCleaner
    from src.config import CharacterCleanerConfig, SentenceCleanerConfig
    from src.file_manager import FileManager
    from src.workflow_engine import WorkflowEngine, validate_data_format
except ImportError:
    from .character_cleaner import CharacterCleaner
    from .sentence_cleaner import SentenceCleaner
    from .config import CharacterCleanerConfig, SentenceCleanerConfig
    from .file_manager import FileManager
    from .workflow_engine import WorkflowEngine, validate_data_format

# 日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 应用
app = FastAPI(title="数据处理API(FastAPI)", description="提供字符清理与句子清理的统一API", version="1.0.0")

# CORS
if CORSMiddleware:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"]
        , allow_headers=["*"]
    )

# 资源与实例
PROJECT_ROOT = Path(__file__).parent.parent
PAGE_DIR = PROJECT_ROOT / 'page'
STATIC_FOLDER = PAGE_DIR
UPLOAD_FOLDER = PAGE_DIR / 'uploads'
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)

# 简易版文件名安全化（避免引入额外依赖）
def secure_filename(name: str) -> str:
    if not name:
        return 'uploaded.json'
    # 仅保留常见安全字符，其他替换为下划线
    name = os.path.basename(name)
    name = re.sub(r"[^A-Za-z0-9._-]+", "_", name)
    # 避免空文件名
    return name or 'uploaded.json'

character_cleaner = CharacterCleaner()
sentence_cleaner = SentenceCleaner()
file_manager = FileManager(UPLOAD_FOLDER)
workflow_engine = WorkflowEngine(character_cleaner, sentence_cleaner)

API_VERSION = "1.0.0"
API_INFO = {
    "name": "数据处理API(FastAPI)",
    "version": API_VERSION,
    "author": "jena",
    "description": "提供字符清理和句子清理的统一API接口",
    "endpoints": [
        "/api/v1/character/clean",
        "/api/v1/sentence/clean",
        "/api/v1/health",
        "/api/v1/info",
        "/api/v1/stats",
        "/api/v1/reset"
    ]
}


def create_response(success: bool, data: Any = None, message: str = "", error_code: str = None) -> Dict[str, Any]:
    return {
        "success": success,
        "timestamp": datetime.now().isoformat(),
        "version": API_VERSION,
        **({"data": data} if success else {"error": {"message": message, "code": error_code or "UNKNOWN_ERROR"}}),
        **({"message": message} if success and message else {}),
    }


def validate_request_data(data: Dict[str, Any], required_fields: List[str]) -> Optional[str]:
    if not data:
        return "请求体不能为空"
    for field in required_fields:
        if field not in data:
            return f"缺少必需字段: {field}"
    return None


# ===== 静态页面 =====
# 说明：为避免静态挂载拦截 /api 前缀导致接口 404，需在注册完 API 路由后再挂载静态资源。
#      Starlette 路由按添加顺序匹配，过早在根路径挂载会覆盖后续的 /api 路径。


# ===== 基础信息端点 =====
@app.get('/api/v1/health')
async def health_check():
    return create_response(True, {"status": "healthy", "uptime": "running"}, "API服务运行正常")


@app.get('/api/v1/info')
async def api_info():
    return create_response(True, API_INFO, "API信息获取成功")


@app.get('/api/v1/stats')
async def get_stats():
    try:
        stats_data = {
            "character_cleaner": character_cleaner.get_stats(),
            "sentence_cleaner": sentence_cleaner.get_stats()
        }
        return create_response(True, stats_data, "统计信息获取成功")
    except Exception as e:
        logger.error(f"获取统计信息失败: {e}")
        return JSONResponse(create_response(False, None, f"获取统计信息失败: {e}", "STATS_ERROR"), status_code=500)


# ===== 兼容旧前端健康检查 =====
@app.get('/api/health')
async def frontend_health_check():
    return {
        "frontend": {"status": "healthy", "port": 5000, "description": "工作流编辑器前后端合并服务"},
        "backend": {"status": "healthy"}
    }


# ===== 文件上传/下载 =====
# 说明：在离线环境下可能缺少 python-multipart 依赖。
# 若缺失，则提供降级的 /api/upload 端点，支持 application/json 与 application/octet-stream，
# 并对 multipart/form-data 给出明确的安装提示。

# 检查是否支持 multipart/form-data
try:
    import multipart  # type: ignore
    _HAS_MULTIPART = True
except Exception:
    _HAS_MULTIPART = False

if _HAS_MULTIPART:
    from fastapi import UploadFile, File

    @app.post('/api/upload')
    async def upload_file(file: UploadFile = File(...)):
        try:
            if not file or not file.filename:
                return JSONResponse({"success": False, "error": "未选择文件"}, status_code=400)
            if not file_manager.allowed_file(file.filename):
                return JSONResponse({"success": False, "error": "不支持的文件类型，请上传JSON文件"}, status_code=400)

            file_id = str(uuid.uuid4())
            # FileManager兼容FastAPI的UploadFile
            saved_path = file_manager.save_uploaded_file(file, file_id)
            if not saved_path:
                return JSONResponse({"success": False, "error": "文件保存失败"}, status_code=500)

            data = file_manager.load_file_data(file_id)
            if data is None:
                return JSONResponse({"success": False, "error": "无效的JSON文件格式"}, status_code=400)
            if not isinstance(data, list):
                return JSONResponse({"success": False, "error": "JSON文件必须包含一个数组"}, status_code=400)

            logger.info(f"文件上传成功: {file.filename}, 数据条数: {len(data)}")
            return {
                "success": True,
                "file_id": file_id,
                "filename": file.filename,
                "data_count": len(data),
                "message": "文件上传成功"
            }
        except Exception as e:
            logger.error(f"文件上传异常: {e}")
            return JSONResponse({"success": False, "error": f"文件上传异常: {e}"}, status_code=500)
else:
    @app.post('/api/upload')
    async def upload_file_fallback(request: Request):
        """
        降级版本的上传接口：
        - 支持 application/json：{"filename": "xxx.json", "content": <数组或JSON字符串>}
        - 支持 application/octet-stream 或 text/plain：通过 ?filename=xxx.json 指定文件名，直接提交文件内容
        - 不支持 multipart/form-data：将返回明确的安装指引
        """
        try:
            ct = request.headers.get('content-type', '')
            if 'multipart/form-data' in ct:
                return JSONResponse({
                    "success": False,
                    "error": "当前环境未安装 python-multipart，无法解析表单文件上传。请将离线包安装到 offline_packages 后重试。",
                    "install_hint": "将离线实现放置到 offline_packages/python_multipart 或 offline_packages/multipart 并重启服务器",
                }, status_code=400)

            file_id = str(uuid.uuid4())
            filename = request.query_params.get('filename') or 'uploaded.json'
            filename = secure_filename(filename)
            target_path = UPLOAD_FOLDER / f"{file_id}_{filename}"

            if 'application/json' in ct:
                body = await request.json()
                # 支持 content 为字符串或对象
                content = body.get('content')
                if content is None:
                    return JSONResponse({"success": False, "error": "JSON内容缺失(content)"}, status_code=400)
                if isinstance(content, (dict, list)):
                    json_text = json.dumps(content, ensure_ascii=False)
                elif isinstance(content, str):
                    json_text = content
                else:
                    return JSONResponse({"success": False, "error": "content 字段类型不支持"}, status_code=400)
                with open(target_path, 'w', encoding='utf-8') as f:
                    f.write(json_text)
            else:
                # 作为原始字节写入
                raw = await request.body()
                with open(target_path, 'wb') as f:
                    f.write(raw)

            # 注册文件元信息，供 FileManager 读取与后续工作流使用
            file_manager.uploaded_files[file_id] = {
                "filename": filename,
                "filepath": str(target_path),
                "original_name": filename,
            }

            # 加载并校验
            data = file_manager.load_file_data(file_id)
            if data is None:
                return JSONResponse({"success": False, "error": "无效的JSON文件格式"}, status_code=400)
            if not isinstance(data, list):
                return JSONResponse({"success": False, "error": "JSON文件必须包含一个数组"}, status_code=400)

            logger.info(f"文件上传成功(降级): {filename}, 数据条数: {len(data)}")
            return {
                "success": True,
                "file_id": file_id,
                "filename": filename,
                "data_count": len(data),
                "message": "文件上传成功(降级模式)"
            }
        except Exception as e:
            logger.error(f"文件上传异常(降级): {e}")
            return JSONResponse({"success": False, "error": f"文件上传异常: {e}"}, status_code=500)


@app.get('/api/download/{filename:path}')
async def download_file(filename: str):
    target = UPLOAD_FOLDER / filename
    if not target.exists():
        return JSONResponse({"success": False, "error": "文件不存在"}, status_code=404)
    return FileResponse(str(target), filename=os.path.basename(str(target)))


@app.get('/api/file/{file_id}/info')
async def get_file_info(file_id: str):
    info = file_manager.get_file_info(file_id)
    if not info:
        return JSONResponse({"success": False, "error": "文件不存在"}, status_code=404)
    data = file_manager.load_file_data(file_id)
    data_count = len(data) if data else 0
    return {"success": True, "file_info": {"filename": info['original_name'], "data_count": data_count}}


# ===== 工作流执行与验证 =====
@app.post('/api/workflow/execute')
async def execute_workflow(request: Request):
    try:
        workflow_data = await request.json()
        if not workflow_data:
            return JSONResponse({"success": False, "error": "无效的工作流数据"}, status_code=400)

        nodes = workflow_data.get('nodes', [])
        input_nodes = [n for n in nodes if n.get('type') == 'input']
        output_nodes = [n for n in nodes if n.get('type') == 'output']
        if not input_nodes:
            return JSONResponse({"success": False, "error": "工作流中缺少输入节点"}, status_code=400)
        if not output_nodes:
            return JSONResponse({"success": False, "error": "工作流中缺少输出节点"}, status_code=400)

        input_node = input_nodes[0]
        cfg = input_node.get('config', {}) or {}
        file_id = cfg.get('fileId')
        format_type = (cfg.get('format') or 'pretrain')
        if not file_id:
            return JSONResponse({"success": False, "error": "输入节点未配置文件ID，请先上传数据文件"}, status_code=400)

        input_data = file_manager.load_file_data(file_id)
        if input_data is None:
            return JSONResponse({"success": False, "error": f"无法加载输入数据，文件ID: {file_id}"}, status_code=400)
        if not isinstance(input_data, list):
            return JSONResponse({"success": False, "error": "输入数据必须为JSON数组"}, status_code=400)

        ok, err = validate_data_format(input_data, format_type)
        if not ok:
            return JSONResponse({"success": False, "error": err}, status_code=400)

        logger.info(f"开始执行工作流，节点数: {len(nodes)}，输入条数: {len(input_data)}")
        exec_result = workflow_engine.execute(workflow_data, input_data, format_type)

        final_data = exec_result.get("final_data", [])
        processing_results = exec_result.get("processing_results", [])

        output_file_info = None
        input_file_info = file_manager.get_file_info(file_id)
        if input_file_info and isinstance(final_data, list):
            try:
                original_filename = input_file_info.get('original_name') or 'output.json'
                name_without_ext = os.path.splitext(original_filename)[0]
                output_filename = name_without_ext + '_processed.json'
                output_file_path = UPLOAD_FOLDER / output_filename
                with open(output_file_path, 'w', encoding='utf-8') as f:
                    json.dump(final_data, f, ensure_ascii=False, indent=2)
                output_file_info = {
                    'filename': output_filename,
                    'download_url': f'/api/download/{output_filename}',
                    'file_path': str(output_file_path)
                }
                logger.info(f"输出数据已保存到: {output_file_path}")
            except Exception as se:
                logger.warning(f"保存输出文件失败: {se}")
                output_file_info = {'error': f"保存失败: {str(se)}"}

        return {
            "success": True,
            "message": "工作流执行完成",
            "results": {
                "input_count": len(input_data),
                "output_count": len(final_data) if isinstance(final_data, list) else 0,
                "output_data": final_data,
                "processing_steps": processing_results,
                "output_file": output_file_info,
                "file_info": input_file_info
            }
        }
    except Exception as e:
        logger.error(f"工作流执行失败: {e}\n{traceback.format_exc()}")
        return JSONResponse({"success": False, "error": f"工作流执行失败: {e}"}, status_code=500)


@app.post('/api/workflow/validate')
async def validate_workflow(request: Request):
    try:
        workflow_data = await request.json()
        if not workflow_data:
            return JSONResponse({"valid": False, "errors": ["无效的工作流数据"], "warnings": []}, status_code=400)

        nodes = workflow_data.get('nodes', [])
        connections = workflow_data.get('connections', [])
        errors: List[str] = []
        warnings: List[str] = []

        if not isinstance(nodes, list) or not isinstance(connections, list):
            return JSONResponse({"valid": False, "errors": ["nodes 或 connections 格式错误"], "warnings": []}, status_code=400)

        input_nodes = [n for n in nodes if n.get('type') == 'input']
        output_nodes = [n for n in nodes if n.get('type') == 'output']
        if not input_nodes:
            errors.append("工作流中缺少输入节点")
        if not output_nodes:
            errors.append("工作流中缺少输出节点")

        data_count = 0
        if input_nodes:
            input_node = input_nodes[0]
            cfg = input_node.get('config', {}) or {}
            file_id = cfg.get('fileId')
            format_type = (cfg.get('format') or 'pretrain').lower()
            if not file_id:
                errors.append("输入节点未配置文件ID，请先上传数据文件")
            else:
                input_data = file_manager.load_file_data(file_id)
                if input_data is None:
                    errors.append(f"无法加载输入数据，文件ID: {file_id}")
                elif not isinstance(input_data, list):
                    errors.append("输入数据必须为JSON数组")
                else:
                    data_count = len(input_data)
                    ok, err = validate_data_format(input_data, format_type)
                    if not ok:
                        errors.append(err or "输入数据格式校验失败")

        has_char = any(n.get('type') == 'character-cleaner' for n in nodes)
        has_sent = any(n.get('type') == 'sentence-cleaner' for n in nodes)
        if not has_char:
            warnings.append("建议添加字符清洗节点以进行格式与隐私清理")
        if not has_sent:
            warnings.append("建议添加句子筛选节点以提升数据质量")
        if not connections:
            warnings.append("尚未创建节点连接，可能导致数据无法流转")

        return {
            "valid": len(errors) == 0,
            "errors": errors,
            "warnings": warnings,
            "summary": {"nodes": len(nodes), "connections": len(connections), "input_items": data_count}
        }
    except Exception as e:
        logger.error(f"工作流验证失败: {e}\n{traceback.format_exc()}")
        return JSONResponse({"valid": False, "errors": [f"工作流验证异常: {e}"], "warnings": []}, status_code=500)


# ===== 处理端点 =====
@app.post('/api/v1/character/clean')
async def character_clean(request: Request):
    try:
        data = await request.json()
        error_msg = validate_request_data(data, ['data', 'format'])
        if error_msg:
            return JSONResponse(create_response(False, None, error_msg, "INVALID_REQUEST"), status_code=400)

        json_data = data['data']
        format_type = data['format'].lower()
        config_data = data.get('config', {})
        if format_type not in ['pretrain', 'sft', 'kto', 'dpo']:
            return JSONResponse(create_response(False, None, "format字段必须是: pretrain, sft, kto, dpo 之一", "INVALID_FORMAT"), status_code=400)
        if not isinstance(json_data, list):
            return JSONResponse(create_response(False, None, "data字段必须是数组", "INVALID_DATA_TYPE"), status_code=400)

        try:
            config = CharacterCleanerConfig(**config_data)
        except Exception as e:
            return JSONResponse(create_response(False, None, f"配置参数错误: {e}", "INVALID_CONFIG"), status_code=400)

        def clean_text_field(text):
            if isinstance(text, str) and text.strip():
                return character_cleaner.process_text(text, config)
            return text

        cleaned_data = []
        processed_count = 0
        for item in json_data:
            if not isinstance(item, dict):
                cleaned_data.append(item)
                continue
            cleaned_item = item.copy()
            if format_type == 'pretrain':
                if 'text' in cleaned_item:
                    cleaned_item['text'] = clean_text_field(cleaned_item['text'])
                    processed_count += 1
            elif format_type == 'sft':
                for field in ['instruction', 'input', 'output', 'system']:
                    if field in cleaned_item:
                        cleaned_item[field] = clean_text_field(cleaned_item[field])
                        processed_count += 1
            elif format_type == 'kto':
                for field in ['instruction', 'input', 'output']:
                    if field in cleaned_item:
                        cleaned_item[field] = clean_text_field(cleaned_item[field])
                        processed_count += 1
            elif format_type == 'dpo':
                for field in ['instruction', 'input', 'chosen', 'rejected']:
                    if field in cleaned_item:
                        cleaned_item[field] = clean_text_field(cleaned_item[field])
                        processed_count += 1
            cleaned_data.append(cleaned_item)

        result = {
            "original_data": json_data,
            "cleaned_data": cleaned_data,
            "format": format_type,
            "total_items": len(json_data),
            "processed_fields": processed_count,
            "stats": character_cleaner.get_stats()
        }
        return create_response(True, result, f"JSON字段清洗完成，处理了{len(json_data)}个条目，{processed_count}个字段")
    except Exception as e:
        logger.error(f"JSON字段清洗失败: {e}\n{traceback.format_exc()}")
        return JSONResponse(create_response(False, None, f"JSON字段清洗失败: {e}", "PROCESSING_ERROR"), status_code=500)


@app.post('/api/v1/sentence/clean')
async def sentence_clean(request: Request):
    try:
        data = await request.json()
        error_msg = validate_request_data(data, ['data', 'format'])
        if error_msg:
            return JSONResponse(create_response(False, None, error_msg, "INVALID_REQUEST"), status_code=400)

        json_data = data['data']
        format_type = data['format'].lower()
        config_data = data.get('config', {})
        if format_type != 'pretrain':
            return JSONResponse(create_response(False, None, "句子清理功能当前仅支持pretrain格式", "UNSUPPORTED_FORMAT"), status_code=400)
        if not isinstance(json_data, list):
            return JSONResponse(create_response(False, None, "data字段必须是数组", "INVALID_DATA_TYPE"), status_code=400)

        try:
            config = SentenceCleanerConfig(**config_data)
        except Exception as e:
            return JSONResponse(create_response(False, None, f"配置参数错误: {e}", "INVALID_CONFIG"), status_code=400)

        texts_to_filter: List[str] = []
        for item in json_data:
            if isinstance(item, dict) and 'text' in item:
                if isinstance(item['text'], str):
                    texts_to_filter.append(item['text'])
                else:
                    texts_to_filter.append("")
            else:
                texts_to_filter.append("")

        original_count = len(texts_to_filter)
        filtered_texts = sentence_cleaner.filter_sentences(texts_to_filter, config)
        final_count = len(filtered_texts)
        filtered_data = [{"text": t} for t in filtered_texts]

        result = {
            "original_data": json_data,
            "filtered_data": filtered_data,
            "format": format_type,
            "original_count": original_count,
            "final_count": final_count,
            "removed_count": original_count - final_count,
            "removal_rate": round((original_count - final_count) / original_count * 100, 2) if original_count > 0 else 0,
            "stats": sentence_cleaner.get_stats()
        }
        return create_response(True, result, f"句子筛选完成，处理了{original_count}个句子，保留了{final_count}个")
    except Exception as e:
        logger.error(f"句子筛选失败: {e}\n{traceback.format_exc()}")
        return JSONResponse(create_response(False, None, f"句子筛选失败: {e}", "PROCESSING_ERROR"), status_code=500)


@app.post('/api/v1/reset')
async def reset_stats():
    try:
        character_cleaner.reset_stats()
        sentence_cleaner.reset_stats()
        return create_response(True, None, "统计信息已重置")
    except Exception as e:
        logger.error(f"重置统计信息失败: {e}")
        return JSONResponse(create_response(False, None, f"重置统计信息失败: {e}", "RESET_ERROR"), status_code=500)


# 可选：简单的404处理（FastAPI默认会返回详细信息，这里统一响应结构）
from fastapi.requests import Request as _Req
from fastapi.responses import Response as _Resp
from fastapi import status


@app.middleware("http")
async def add_default_error_structure(request: _Req, call_next):
    try:
        response = await call_next(request)
        return response
    except Exception as e:
        logger.error(f"内部服务器错误: {e}")
        return JSONResponse(create_response(False, None, "内部服务器错误", "INTERNAL_ERROR"), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)

# 在所有路由注册完成后再挂载静态资源，避免覆盖 /api 前缀
if StaticFiles:
    app.mount("/", StaticFiles(directory=str(STATIC_FOLDER), html=True), name="static")


if __name__ == '__main__':
    uvicorn = deps.get_uvicorn()
    if uvicorn is None:
        raise ImportError("Uvicorn不可用，无法启动FastAPI服务器。")
    logger.info("启动 FastAPI 数据处理服务器...")
    uvicorn.run("src.api_server_fastapi:app", host="0.0.0.0", port=5000, reload=False)